﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Linq2Crm.ObjectModel;
using Linq2Crm.ObjectModel.Attributes;
using Linq2Crm.ObjectModel.DomainObject;
using SilverCrmSoap.CrmSdk;


namespace Linq2Crm
{
   internal class DomainObjectFactory
   {
      private readonly IEnumerable<AttributeMetadata> attributes;

      public DomainObjectFactory(IEnumerable<AttributeMetadata> attributes)
      {
         this.attributes = attributes;
      }

      public IDomainObject CreateDomainObjectFrom(Entity entity, Type domainObjectType)
      {
         IDomainObject domainObject = (IDomainObject)Activator.CreateInstance(domainObjectType);

         domainObject.ConvertingFromEntity();

         domainObject.Id = entity.Id;

         List<PropertyInfo> properties = domainObject.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();

         Dictionary<string, PropertyInfo> propertiesMap = new Dictionary<string, PropertyInfo>();
         object entObjAttr = domainObject.GetType().GetCustomAttributes(typeof(CrmEntityMappingAttribute), true).FirstOrDefault();
         if (entObjAttr != null)
         {
            CrmEntityMappingAttribute mappingData = (CrmEntityMappingAttribute)entObjAttr;
            propertiesMap.Add(mappingData.IDField, properties.First(x => x.Name == "Id"));
            if (!String.IsNullOrEmpty(mappingData.NameField))
            {
               propertiesMap.Add(mappingData.NameField, properties.First(x => x.Name == "Name"));
            }
            propertiesMap.Add("statecode", properties.First(x => x.Name == "StateCode"));
            propertiesMap.Add("statuscode", properties.First(x => x.Name == "StatusCode"));
         }

         properties.ForEach(x =>
                              {
                                 object attribute = x.GetCustomAttributes(typeof(CrmFieldMappingAttribute), false).FirstOrDefault();
                                 if (attribute != null)
                                 {
                                    string name = ((CrmFieldMappingAttribute)attribute).SchemaName;
                                    if (!propertiesMap.ContainsKey(name))
                                    {
                                       propertiesMap.Add(name, null);
                                    }
                                    propertiesMap[name] = x;
                                 }
                              });

         foreach (var attribute in entity.Attributes)
         {
            if (propertiesMap.ContainsKey(attribute.Key))
            {
               object value = GetValue(attribute.Value, attribute.Key, propertiesMap[attribute.Key].PropertyType);
               if (value != null)
               {
                  try
                  {
                     propertiesMap[attribute.Key].SetValue(domainObject, value, null);
                  }
                  catch (Exception e)
                  {
                     throw new ArgumentException(string.Format("Attempt to write [{0}] value to field [{1}] with type [{2}] has failed", value.GetType(), attribute.Key, propertiesMap[attribute.Key].PropertyType), e);
                  }
               }
            }
         }

         domainObject.OrigStatusCode = domainObject.StatusCode;

         domainObject.ConvertedFromEntity();			//NOTE: To put try catch or not to put? There is the question!

         return domainObject;
      }

      private object GetValue(object value, string key, Type targetType)
      {
         AttributeMetadata attribute = attributes.FirstOrDefault(x => x.LogicalName == key);
         if (attribute == null)
         {
            Debug.Assert(false);
         }

         Type sourceType = value.GetType();

         if (sourceType.Name == targetType.Name)
         {
            return value;
         }

         object result = Activator.CreateInstance(targetType);

         try
         {
            switch (attribute.AttributeType.Value)
            {
               case AttributeTypeCode.Lookup:
               case AttributeTypeCode.Owner:
                  EntityReference reference = value as EntityReference;
                  if (reference != null)
                  {
                     result = reference.Id;
                  }
                  break;
               case AttributeTypeCode.Money:
                  Money money = value as Money;
                  if (money != null)
                  {
                     result = money.Value;
                  }
                  break;
               case AttributeTypeCode.Picklist:
                  OptionSetValue optionSet = value as OptionSetValue;
                  if (optionSet != null)
                  {
                     result = optionSet.Value;
                  }
                  break;
               case AttributeTypeCode.State:
               case AttributeTypeCode.Status:
                  OptionSetValue stateValue = value as OptionSetValue;
                  if (stateValue != null)
                  {
                     result = stateValue.Value;
                  }
                  else
                  {
                     Debug.Assert(false);
                  }
                  break;
               default:
                  result = value;
                  break;
            }
            return result;
         }
         catch (Exception ex)
         {
            System.Diagnostics.Debug.Assert(false, ex.Message);
         }
         return null;
      }

      public Entity CreateCrmEntityFrom(IDomainObject domainObject, CheckType check = CheckType.NoCheck)
      {
			Entity resultEntity = new Entity()
			{
				Attributes = new AttributeCollection(),
				FormattedValues = new FormattedValueCollection(),
				RelatedEntities = new RelatedEntityCollection()
			};

         domainObject.ConvertingToEntity();

         List<PropertyInfo> properties = domainObject.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();

         object entObjAttr = domainObject.GetType().GetCustomAttributes(typeof(CrmEntityMappingAttribute), true).FirstOrDefault();
         CrmEntityMappingAttribute mappingData = null;
         if (entObjAttr != null)
         {
            mappingData = (CrmEntityMappingAttribute)entObjAttr;
            resultEntity.LogicalName = mappingData.EntitySchemaName;
            if (!string.IsNullOrEmpty(mappingData.NameField))
            {
               object crmValue = null;
					GetCrmValue(mappingData.NameField, domainObject.Name, check, ref crmValue, mappingData.EntitySchemaName);
               resultEntity.Attributes.Add(new SilverCrmSoap.CrmSdk.KeyValuePair<string, object>(){Key = mappingData.NameField,Value = crmValue});
            }
            resultEntity.Id = domainObject.Id;
         }

         properties.ForEach(x =>
                              {
                                 object attribute = x.GetCustomAttributes(typeof(CrmFieldMappingAttribute), true).FirstOrDefault();
                                 if (attribute != null && (mappingData.IgnoreProperties == null || !mappingData.IgnoreProperties.Contains(x.Name)))
                                 {
                                    string name = ((CrmFieldMappingAttribute)attribute).SchemaName;
                                    if (name.ToLower().Equals("statecode") || name.ToLower().Equals("statuscode")) // this is updated with special request
                                    {
                                       return;
                                    }

                                    object value = x.GetValue(domainObject, null);
                                    //if (value == null)
                                    //{
                                    //   return;
                                    //}
                                    object crmValue = null;
												if (GetCrmValue(name, value, check, ref crmValue, ((CrmFieldMappingAttribute)attribute).SchemaName))
                                    {
                                       resultEntity.Attributes.Add(new SilverCrmSoap.CrmSdk.KeyValuePair<string, object>(){Key = name,Value = crmValue});
                                    }
                                 }
                              });

         domainObject.ConvertedToEntity();

         return resultEntity;
      }

      private bool GetCrmValue(string key, object sourceValue, CheckType check, ref object crmValue, string schemaName)
      {
         AttributeMetadata attribute = attributes.FirstOrDefault(x => x.LogicalName == key);
         if (attribute == null)
         {
            Debug.Assert(false);
				throw new ArgumentNullException(string.Format("Filed '{0}' is not present in entyty {1}", key, schemaName));
         }

         switch (check)
         {
            case CheckType.NoCheck:
               break;
            case CheckType.Inserting:
               if (attribute.IsValidForCreate.HasValue)
               {
                  if (!attribute.IsValidForCreate.Value)
                  {
                     return false;
                  }
               }
               break;
            case CheckType.Updating:
               if (attribute.IsValidForUpdate.HasValue)
               {
                  if (!attribute.IsValidForUpdate.Value)
                  {
                     return false;
                  }
               }
               break;
            default:
               throw new ArgumentOutOfRangeException("check");
         }

         try
         {
            switch (attribute.AttributeType.Value)
            {
               case AttributeTypeCode.Lookup:
               case AttributeTypeCode.Owner:
                  if (sourceValue == null)
                  {
                     return true;
                  }
                  if (sourceValue.GetType().Name == typeof(Guid).Name)
                  {
							
                     crmValue = new EntityReference(){ LogicalName = ((LookupAttributeMetadata)attribute).Targets[0], Id = (Guid)sourceValue};
                  }
                  else if (sourceValue.GetType().Name == typeof(EntityReference).Name)
                  {
                     crmValue = new EntityReference(){LogicalName = ((LookupAttributeMetadata)attribute).Targets[0],Id = ((EntityReference)sourceValue).Id};
                  }
                  else
                  {
                     Debug.Assert(false);
                  }
                  if (((EntityReference)crmValue).Id == Guid.Empty)
                  {
                     crmValue = null;
                  }
                  return true;
               case AttributeTypeCode.Money:
                  if (sourceValue == null)
                  {
                     return false;
                  }
                  Money money = new Money();
                  decimal moneyValue = 0;
                  if (sourceValue is decimal)
                  {
                     moneyValue = (decimal)sourceValue;
                  }
                  else if (sourceValue is int)
                  {
                     moneyValue = new decimal((int)sourceValue);
                  }
                  else
                  {
                     Debug.Assert(false);
                  }
                  if (((MoneyAttributeMetadata)attribute).MaxValue.HasValue && moneyValue > new decimal(((MoneyAttributeMetadata)attribute).MaxValue.Value))
                  {
                     moneyValue = new decimal(((MoneyAttributeMetadata)attribute).MaxValue.Value);
                  }
                  if (((MoneyAttributeMetadata)attribute).MinValue.HasValue && moneyValue < new decimal(((MoneyAttributeMetadata)attribute).MinValue.Value))
                  {
                     moneyValue = new decimal(((MoneyAttributeMetadata)attribute).MinValue.Value);
                  }
                  money.Value = moneyValue;

                  crmValue = money;
                  return true;
               case AttributeTypeCode.Picklist:
                  if (sourceValue == null)
                  {
                     return false;
                  }
                  var pickListValues = ((PicklistAttributeMetadata)attribute).OptionSet.Options;
                  if (pickListValues.Any(x => x.Value == (int)sourceValue))
                  {
                     OptionSetValue optionSet = new OptionSetValue();
                     optionSet.Value = (int)sourceValue;
                     crmValue = optionSet;
                     return true;
                  }
                  return false;
               case AttributeTypeCode.State:
               case AttributeTypeCode.Status: // maybe there's need to check available statuses				
                  if (sourceValue == null)
                  {
                     return false;
                  }
                  if (!(sourceValue is int))
                  {
                     Debug.Assert(false);
                  }
                  crmValue = sourceValue;
                  return true;
               case AttributeTypeCode.Double:
                  double? doubleValue = (double?)sourceValue;
                  if (((DoubleAttributeMetadata)attribute).MaxValue.HasValue && doubleValue > ((DoubleAttributeMetadata)attribute).MaxValue.Value)
                  {
                     doubleValue = ((DoubleAttributeMetadata)attribute).MaxValue.Value;
                  }
                  if (((DoubleAttributeMetadata)attribute).MinValue.HasValue && doubleValue < ((DoubleAttributeMetadata)attribute).MinValue.Value)
                  {
                     doubleValue = ((MoneyAttributeMetadata)attribute).MinValue.Value;
                  }
                  crmValue = doubleValue;
                  return true;
               case AttributeTypeCode.String:
                  string value = sourceValue as string;
                  if (value != null && (((StringAttributeMetadata)attribute).MaxLength.HasValue && value.Length > ((StringAttributeMetadata)attribute).MaxLength.Value))
                  {
                     value = value.Substring(0, ((StringAttributeMetadata)attribute).MaxLength.Value);
                  }
                  crmValue = value;
                  return true;
               case AttributeTypeCode.Memo:
                  string memoValue = sourceValue as string;
                  if (memoValue != null && (((MemoAttributeMetadata)attribute).MaxLength.HasValue && memoValue.Length > ((MemoAttributeMetadata)attribute).MaxLength.Value))
                  {
                     memoValue = memoValue.Substring(0, ((MemoAttributeMetadata)attribute).MaxLength.Value);
                  }
                  crmValue = memoValue;
                  return true;
               case AttributeTypeCode.BigInt:
                  long bigValue = Convert.ToInt64(sourceValue);
                  if (((BigIntAttributeMetadata)attribute).MaxValue.HasValue && bigValue > ((BigIntAttributeMetadata)attribute).MaxValue.Value)
                  {
                     bigValue = ((BigIntAttributeMetadata)attribute).MaxValue.Value;
                  }
                  else if (((BigIntAttributeMetadata)attribute).MinValue.HasValue && bigValue < ((BigIntAttributeMetadata)attribute).MinValue.Value)
                  {
                     bigValue = ((BigIntAttributeMetadata)attribute).MinValue.Value;
                  }
                  crmValue = bigValue;
                  return true;
               case AttributeTypeCode.Decimal:
                  decimal decValue = Convert.ToDecimal(sourceValue);
                  if (((DecimalAttributeMetadata)attribute).MaxValue.HasValue && decValue > ((DecimalAttributeMetadata)attribute).MaxValue.Value)
                  {
                     decValue = ((DecimalAttributeMetadata)attribute).MaxValue.Value;
                  }
                  else if (((DecimalAttributeMetadata)attribute).MinValue.HasValue && decValue < ((DecimalAttributeMetadata)attribute).MinValue.Value)
                  {
                     decValue = ((DecimalAttributeMetadata)attribute).MinValue.Value;
                  }
                  crmValue = decValue;
                  return true;
               case AttributeTypeCode.Integer:
                  int intValue = Convert.ToInt32(sourceValue);
                  if (((IntegerAttributeMetadata)attribute).MaxValue.HasValue && intValue > ((IntegerAttributeMetadata)attribute).MaxValue.Value)
                  {
                     intValue = ((IntegerAttributeMetadata)attribute).MaxValue.Value;
                  }
                  else if (((IntegerAttributeMetadata)attribute).MinValue.HasValue && intValue < ((IntegerAttributeMetadata)attribute).MinValue.Value)
                  {
                     intValue = ((IntegerAttributeMetadata)attribute).MinValue.Value;
                  }
                  crmValue = intValue;
                  return true;
               default:
                  crmValue = sourceValue;
                  return true;
            }
         }
         catch (Exception ex)
         {
            Debug.Assert(false, ex.Message);
         }
         crmValue = sourceValue;
         return true;
      }

      public Dictionary<RecordStateCode, Dictionary<int, string>> GetStatusList(int language = 0)
      {
         AttributeMetadata metadata = attributes.FirstOrDefault(x => x.LogicalName.ToLower().Equals("statuscode"));
         if (metadata == null)
         {
            return null;
         }

         OptionSetMetadata optionSet = ((StatusAttributeMetadata)metadata).OptionSet;

         Dictionary<RecordStateCode, Dictionary<int, string>> result = new Dictionary<RecordStateCode, Dictionary<int, string>>();
         foreach (StatusOptionMetadata option in optionSet.Options)
         {
            LocalizedLabel label = option.Label.LocalizedLabels.FirstOrDefault(x => x.LanguageCode == language) ?? option.Label.UserLocalizedLabel;
            if (option.Value != null)
            {
               if (!result.ContainsKey((RecordStateCode)option.State.Value))
               {
                  result.Add((RecordStateCode)option.State.Value, new Dictionary<int, string>());
               }
               result[(RecordStateCode)option.State.Value].Add(option.Value.Value, label == null
                                                                                       ? ""
                                                                                       : label.Label);
            }
         }
         return result;
      }

      public Dictionary<RecordStateCode, int> GetDefaultStatuses(int language = 0)
      {
         AttributeMetadata metadata = attributes.FirstOrDefault(x => x.LogicalName.ToLower().Equals("statecode"));
         if (metadata == null)
         {
            return null;
         }

         OptionSetMetadata optionSet = ((StateAttributeMetadata)metadata).OptionSet;

         Dictionary<RecordStateCode, int> result = new Dictionary<RecordStateCode, int>();
         foreach (StateOptionMetadata option in optionSet.Options)
         {
            if (option.Value != null)
            {
               if (!result.ContainsKey((RecordStateCode)option.Value.Value))
               {
                  result.Add((RecordStateCode)option.Value.Value, option.DefaultStatus.Value);
               }
            }
         }
         return result;
      }

      public Dictionary<int, string> GetOptionSet(string propertySchemaName, int lcid = 0)
      {
         AttributeMetadata metadata = attributes.FirstOrDefault(x => x.LogicalName.ToLower().Equals(propertySchemaName));
         if (metadata == null || !(metadata is PicklistAttributeMetadata))
         {
            return null;
         }

         OptionSetMetadata optionSet = ((PicklistAttributeMetadata)metadata).OptionSet;

         Dictionary<int, string> result = new Dictionary<int, string>();
         foreach (OptionMetadata option in optionSet.Options)
         {
            if (option.Value != null)
            {
               if (!result.ContainsKey(option.Value.Value))
               {
                  var defValue = option.Label.UserLocalizedLabel.Label;                  
                  var localized = option.Label.LocalizedLabels.FirstOrDefault(x => x.LanguageCode == lcid);
                  result.Add(option.Value.Value, localized != null
                                                    ? localized.Label
                                                    : defValue);
               }
            }
         }
         return result;
      }
   }

   internal enum CheckType
   {
      NoCheck,
      Inserting,
      Updating,
   }
}
